Time field
canonInput that parses typed times with a list of quarter-hour options.
acrm !4087 (28 Sep 2026, owner): the visible duration next to times uses `fmtDurShort` — „45 m“ · „1 h“ · „1 h 30 m“ (in the context of time „m“ = minutes). Screen readers keep the full „min“ via `fmtDur` (TimeField option aria-label, DateTimeRange hidden live text) so „m“ is not read as metres. Other durations in the app (timeline, Setkání, tasks, reminders) keep „min“.
Used in 9 files · 7 areas · 15× — components/assistant, components/callwrapup, components/mail, components/meetings, components/tasks, components/timeline …
Default
export const Default = () => {
const [value, setValue] = useState<number | null>(9 * 60 + 30);
return (
<div style={grid}>
<TimeField value={value} onChange={setValue} ariaLabel="Čas" />
</div>
);
};Variants
step 30
export const Variants = () => {
const [start, setStart] = useState<number | null>(9 * 60);
const [end, setEnd] = useState<number | null>(10 * 60 + 30);
const [coarse, setCoarse] = useState<number | null>(14 * 60);
return (
<div style={row}>
<Labeled label="start (step 15)">
<div style={row}>
<TimeField value={start} onChange={setStart} ariaLabel="Od" />
<TimeList from={9 * 60} selected={9 * 60} />
</div>
</Labeled>
<Labeled label="end — getStart, options carry the duration">
<div style={row}>
<TimeField value={end} onChange={setEnd} getStart={() => start} ariaLabel="Do" />
<TimeList start={9 * 60} from={9 * 60 + 15} selected={10 * 60 + 30} />
</div>
</Labeled>
<Labeled label="step 30">
<TimeField value={coarse} onChange={setCoarse} step={30} ariaLabel="Čas" />
</Labeled>
</div>
);
};States
empty (placeholder –)
filled
disabled
export const States = () => {
const [empty, setEmpty] = useState<number | null>(null);
const [filled, setFilled] = useState<number | null>(9 * 60 + 30);
const [disabled, setDisabled] = useState<number | null>(9 * 60 + 30);
return (
<div style={grid}>
<div style={row}>
<Labeled label="empty (placeholder –)">
<TimeField value={empty} onChange={setEmpty} ariaLabel="Čas" />
</Labeled>
<Labeled label="filled">
<TimeField value={filled} onChange={setFilled} ariaLabel="Čas" />
</Labeled>
<Labeled label="disabled">
<TimeField value={disabled} onChange={setDisabled} disabled ariaLabel="Čas" />
</Labeled>
</div>
<span style={{ color: 'var(--fg-muted)', font: 'var(--small)' }}>
Focus: `.dp-field:focus-within` draws the accent border + 3px accent ring; the selected
option in the open list is `.dp-topt.on` (accent ink, 600).
</span>
</div>
);
};Composition
–
1 h 30 m
export const Composition = () => {
const [from, setFrom] = useState<number | null>(9 * 60);
const [to, setTo] = useState<number | null>(10 * 60 + 30);
const length = from != null && to != null && to > from ? to - from : null;
return (
<div style={grid}>
<Labeled label=".dp-timegroup — from, separator, to (getStart), .dp-dur">
<div className="dp-timegroup">
<TimeField value={from} onChange={setFrom} ariaLabel="Od" />
<span className="dp-sep">–</span>
<TimeField value={to} onChange={setTo} getStart={() => from} ariaLabel="Do" />
{length != null && <span className="dp-dur">{fmtDurShort(length)}</span>}
</div>
</Labeled>
<Labeled label="form field — label from <Field>">
<Field label="Čas">
<TimeField value={from} onChange={setFrom} ariaLabel="Čas" />
</Field>
</Labeled>
</div>
);
};